// ========================================================================
// Copyright (c) 1999-2009 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses.
// ========================================================================

package org.eclipse.jetty.servlet;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeaderValues;
import org.eclipse.jetty.http.HttpHeaders;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.io.WriterOutputStream;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Dispatcher;
import org.eclipse.jetty.server.HttpConnection;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.InclusiveByteRange;
import org.eclipse.jetty.server.ResourceCache;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.nio.NIOConnector;
import org.eclipse.jetty.server.ssl.SslConnector;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.MultiPartOutputStream;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.FileResource;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.eclipse.jetty.util.resource.ResourceFactory;

/* ------------------------------------------------------------ */
/**
 * The default servlet. This servlet, normally mapped to /, provides the handling for static content, OPTION and TRACE methods for the context. The following initParameters are supported, these can be set either on the servlet itself or as ServletContext initParameters with a prefix of org.eclipse.jetty.servlet.Default. :
 * 
 * <PRE>
 *  acceptRanges      If true, range requests and responses are
 *                    supported
 * 
 *  dirAllowed        If true, directory listings are returned if no
 *                    welcome file is found. Else 403 Forbidden.
 * 
 *  welcomeServlets   If true, attempt to dispatch to welcome files
 *                    that are servlets, but only after no matching static
 *                    resources could be found. If false, then a welcome
 *                    file must exist on disk. If "exact", then exact
 *                    servlet matches are supported without an existing file.
 *                    Default is true.
 * 
 *                    This must be false if you want directory listings,
 *                    but have index.jsp in your welcome file list.
 * 
 *  redirectWelcome   If true, welcome files are redirected rather than
 *                    forwarded to.
 * 
 *  gzip              If set to true, then static content will be served as
 *                    gzip content encoded if a matching resource is
 *                    found ending with ".gz"
 * 
 *  resourceBase      Set to replace the context resource base
 * 
 *  resourceCache     If set, this is a context attribute name, which the servlet 
 *                    will use to look for a shared ResourceCache instance. 
 *                        
 *  relativeResourceBase
 *                    Set with a pathname relative to the base of the
 *                    servlet context root. Useful for only serving static content out
 *                    of only specific subdirectories.
 * 
 *  pathInfoOnly      If true, only the path info will be applied to the resourceBase 
 *                        
 *  stylesheet	      Set with the location of an optional stylesheet that will be used
 *                    to decorate the directory listing html.
 * 
 *  aliases           If True, aliases of resources are allowed (eg. symbolic
 *                    links and caps variations). May bypass security constraints.
 * 
 *  maxCacheSize      The maximum total size of the cache or 0 for no cache.
 *  maxCachedFileSize The maximum size of a file to cache
 *  maxCachedFiles    The maximum number of files to cache
 * 
 *  useFileMappedBuffer
 *                    If set to true, it will use mapped file buffer to serve static content
 *                    when using NIO connector. Setting this value to false means that
 *                    a direct buffer will be used instead of a mapped file buffer.
 *                    By default, this is set to true.
 * 
 *  cacheControl      If set, all static content will have this value set as the cache-control
 *                    header.
 * 
 * 
 * </PRE>
 */
@SuppressWarnings({ "rawtypes" })
public class DefaultServlet extends HttpServlet implements ResourceFactory
{

	private static final Logger LOG = Log.getLogger(DefaultServlet.class);

	private static final long serialVersionUID = 4930458713846881193L;
	private ServletContext _servletContext;
	private ContextHandler _contextHandler;

	private boolean _acceptRanges = true;
	private boolean _dirAllowed = true;
	private boolean _welcomeServlets = false;
	private boolean _welcomeExactServlets = false;
	private boolean _redirectWelcome = false;
	private boolean _gzip = true;
	private boolean _pathInfoOnly = false;

	private Resource _resourceBase;
	private ResourceCache _cache;

	private MimeTypes _mimeTypes;
	private String[] _welcomes;
	private Resource _stylesheet;
	private boolean _useFileMappedBuffer = false;
	private ByteArrayBuffer _cacheControl;
	private String _relativeResourceBase;
	private ServletHandler _servletHandler;
	private ServletHolder _defaultHolder;

	/* ------------------------------------------------------------ */
	@Override
	public void init()
		throws UnavailableException
	{
		_servletContext = getServletContext();
		_contextHandler = initContextHandler(_servletContext);

		_mimeTypes = _contextHandler.getMimeTypes();

		_welcomes = _contextHandler.getWelcomeFiles();
		if (_welcomes == null)
			_welcomes = new String[] { "index.html", "index.jsp" };

		_acceptRanges = getInitBoolean("acceptRanges", _acceptRanges);
		_dirAllowed = getInitBoolean("dirAllowed", _dirAllowed);
		_redirectWelcome = getInitBoolean("redirectWelcome", _redirectWelcome);
		_gzip = getInitBoolean("gzip", _gzip);
		_pathInfoOnly = getInitBoolean("pathInfoOnly", _pathInfoOnly);

		if ("exact".equals(getInitParameter("welcomeServlets")))
		{
			_welcomeExactServlets = true;
			_welcomeServlets = false;
		}
		else
			_welcomeServlets = getInitBoolean("welcomeServlets", _welcomeServlets);

		if (getInitParameter("aliases") != null)
			_contextHandler.setAliases(getInitBoolean("aliases", false));

		boolean aliases = _contextHandler.isAliases();
		if (!aliases && !FileResource.getCheckAliases())
			throw new IllegalStateException("Alias checking disabled");
		if (aliases)
			_servletContext.log("Aliases are enabled");

		_useFileMappedBuffer = getInitBoolean("useFileMappedBuffer", _useFileMappedBuffer);

		_relativeResourceBase = getInitParameter("relativeResourceBase");

		String rb = getInitParameter("resourceBase");
		if (rb != null)
		{
			if (_relativeResourceBase != null)
				throw new UnavailableException("resourceBase & relativeResourceBase");
			try {
				_resourceBase = _contextHandler.newResource(rb);
			} catch (Exception e)
			{
				LOG.warn(Log.EXCEPTION, e);
				throw new UnavailableException(e.toString());
			}
		}

		String css = getInitParameter("stylesheet");
		try
		{
			if (css != null)
			{
				_stylesheet = Resource.newResource(css);
				if (!_stylesheet.exists())
				{
					LOG.warn("!" + css);
					_stylesheet = null;
				}
			}
			if (_stylesheet == null)
			{
				_stylesheet = Resource.newResource(this.getClass().getResource("/jetty-dir.css"));
			}
		} catch (Exception e)
		{
			LOG.warn(e.toString());
			LOG.debug(e);
		}

		String t = getInitParameter("cacheControl");
		if (t != null)
			_cacheControl = new ByteArrayBuffer(t);

		String resourceCache = getInitParameter("resourceCache");
		int max_cache_size = getInitInt("maxCacheSize", -2);
		int max_cached_file_size = getInitInt("maxCachedFileSize", -2);
		int max_cached_files = getInitInt("maxCachedFiles", -2);
		if (resourceCache != null)
		{
			if (max_cache_size != -1 || max_cached_file_size != -2 || max_cached_files != -2)
				LOG.debug("ignoring resource cache configuration, using resourceCache attribute");
			if (_relativeResourceBase != null || _resourceBase != null)
				throw new UnavailableException("resourceCache specified with resource bases");
			_cache = (ResourceCache)_servletContext.getAttribute(resourceCache);

			LOG.debug("Cache {}={}", resourceCache, _cache);
		}

		try
		{
			if (_cache == null && max_cached_files > 0)
			{
				_cache = new ResourceCache(null, this, _mimeTypes, _useFileMappedBuffer);

				if (max_cache_size > 0)
					_cache.setMaxCacheSize(max_cache_size);
				if (max_cached_file_size >= -1)
					_cache.setMaxCachedFileSize(max_cached_file_size);
				if (max_cached_files >= -1)
					_cache.setMaxCachedFiles(max_cached_files);
			}
		} catch (Exception e)
		{
			LOG.warn(Log.EXCEPTION, e);
			throw new UnavailableException(e.toString());
		}

		_servletHandler = (ServletHandler)_contextHandler.getChildHandlerByClass(ServletHandler.class);
		for (ServletHolder h: _servletHandler.getServlets())
			if (h.getServletInstance() == this)
				_defaultHolder = h;

		if (LOG.isDebugEnabled())
			LOG.debug("resource base = " + _resourceBase);
	}

	/**
	 * Compute the field _contextHandler.<br/>
	 * In the case where the DefaultServlet is deployed on the HttpService it is likely that this method needs to be overwritten to unwrap the ServletContext facade until we reach the original jetty's ContextHandler.
	 * 
	 * @param servletContext The servletContext of this servlet.
	 * @return the jetty's ContextHandler for this servletContext.
	 */
	protected ContextHandler initContextHandler(ServletContext servletContext)
	{
		ContextHandler.Context scontext = ContextHandler.getCurrentContext();
		if (scontext == null)
		{
			if (servletContext instanceof ContextHandler.Context)
				return ((ContextHandler.Context)servletContext).getContextHandler();
			else
				throw new IllegalArgumentException("The servletContext " + servletContext + " " +
					servletContext.getClass().getName() + " is not " + ContextHandler.Context.class.getName());
		}
		else
			return ContextHandler.getCurrentContext().getContextHandler();
	}

	/* ------------------------------------------------------------ */
	@Override
	public String getInitParameter(String name)
	{
		String value = getServletContext().getInitParameter("org.eclipse.jetty.servlet.Default." + name);
		if (value == null)
			value = super.getInitParameter(name);
		return value;
	}

	/* ------------------------------------------------------------ */
	private boolean getInitBoolean(String name, boolean dft)
	{
		String value = getInitParameter(name);
		if (value == null || value.length() == 0)
			return dft;
		return (value.startsWith("t") ||
			value.startsWith("T") ||
			value.startsWith("y") ||
			value.startsWith("Y") || value.startsWith("1"));
	}

	/* ------------------------------------------------------------ */
	private int getInitInt(String name, int dft)
	{
		String value = getInitParameter(name);
		if (value == null)
			value = getInitParameter(name);
		if (value != null && value.length() > 0)
			return Integer.parseInt(value);
		return dft;
	}

	/* ------------------------------------------------------------ */
	/**
	 * get Resource to serve. Map a path to a resource. The default implementation calls HttpContext.getResource but derived servlets may provide their own mapping.
	 * 
	 * @param pathInContext The path to find a resource for.
	 * @return The resource to serve.
	 */
	public Resource getResource(String pathInContext)
	{
		Resource r = null;
		if (_relativeResourceBase != null)
			pathInContext = URIUtil.addPaths(_relativeResourceBase, pathInContext);

		try
		{
			if (_resourceBase != null)
			{
				r = _resourceBase.addPath(pathInContext);
			}
			else
			{
				URL u = _servletContext.getResource(pathInContext);
				r = _contextHandler.newResource(u);
			}

			if (LOG.isDebugEnabled())
				LOG.debug("Resource " + pathInContext + "=" + r);
		} catch (IOException e)
		{
			LOG.ignore(e);
		}

		if ((r == null || !r.exists()) && pathInContext.endsWith("/jetty-dir.css"))
			r = _stylesheet;

		return r;
	}

	/* ------------------------------------------------------------ */
	@SuppressWarnings("null")
	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException
	{
		String servletPath = null;
		String pathInfo = null;
		Enumeration<String> reqRanges = null;
		Boolean included = request.getAttribute(Dispatcher.INCLUDE_REQUEST_URI) != null;
		if (included != null && included.booleanValue())
		{
			servletPath = (String)request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH);
			pathInfo = (String)request.getAttribute(Dispatcher.INCLUDE_PATH_INFO);
			if (servletPath == null)
			{
				servletPath = request.getServletPath();
				pathInfo = request.getPathInfo();
			}
		}
		else
		{
			included = Boolean.FALSE;
			servletPath = _pathInfoOnly ? "/" : request.getServletPath();
			pathInfo = request.getPathInfo();

			// Is this a Range request?
			reqRanges = request.getHeaders(HttpHeaders.RANGE);
			if (!hasDefinedRange(reqRanges))
				reqRanges = null;
		}

		String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
		boolean endsWithSlash = (pathInfo == null ? request.getServletPath() : pathInfo).endsWith(URIUtil.SLASH);

		// Can we gzip this request?
		String pathInContextGz = null;
		boolean gzip = false;
		if (!included.booleanValue() && _gzip && reqRanges == null && !endsWithSlash)
		{
			String accept = request.getHeader(HttpHeaders.ACCEPT_ENCODING);
			if (accept != null && accept.indexOf("gzip") >= 0)
				gzip = true;
		}

		// Find the resource and content
		Resource resource = null;
		HttpContent content = null;

		try
		{
			// Try gzipped content first
			if (gzip)
			{
				pathInContextGz = pathInContext + ".gz";

				if (_cache == null)
				{
					resource = getResource(pathInContextGz);
				}
				else
				{
					content = _cache.lookup(pathInContextGz);
					resource = (content == null) ? null : content.getResource();
				}

				if (resource == null || !resource.exists() || resource.isDirectory())
				{
					gzip = false;
					pathInContextGz = null;
				}
			}

			// find resource
			if (!gzip)
			{
				if (_cache == null)
					resource = getResource(pathInContext);
				else
				{
					content = _cache.lookup(pathInContext);
					resource = content == null ? null : content.getResource();
				}
			}

			if (LOG.isDebugEnabled())
				LOG.debug("uri=" + request.getRequestURI() + " resource=" + resource + (content != null ? " content" : ""));

			// Handle resource
			if (resource == null || !resource.exists())
			{
				if (included)
					throw new FileNotFoundException("!" + pathInContext);
				response.sendError(HttpServletResponse.SC_NOT_FOUND);
			}
			else if (!resource.isDirectory())
			{
				if (endsWithSlash && _contextHandler.isAliases() && pathInContext.length() > 1)
				{
					String q = request.getQueryString();
					pathInContext = pathInContext.substring(0, pathInContext.length() - 1);
					if (q != null && q.length() != 0)
						pathInContext += "?" + q;
					response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(), pathInContext)));
				}
				else
				{
					// ensure we have content
					if (content == null)
						content = new HttpContent.ResourceAsHttpContent(resource, _mimeTypes.getMimeByExtension(resource.toString()), response.getBufferSize());

					if (included.booleanValue() || passConditionalHeaders(request, response, resource, content))
					{
						if (gzip)
						{
							response.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
							String mt = _servletContext.getMimeType(pathInContext);
							if (mt != null)
								response.setContentType(mt);
						}
						sendData(request, response, included.booleanValue(), resource, content, reqRanges);
					}
				}
			}
			else
			{
				String welcome = null;

				if (!endsWithSlash || (pathInContext.length() == 1 && request.getAttribute("org.eclipse.jetty.server.nullPathInfo") != null))
				{
					StringBuffer buf = request.getRequestURL();
					synchronized (buf)
					{
						int param = buf.lastIndexOf(";");
						if (param < 0)
							buf.append('/');
						else
							buf.insert(param, '/');
						String q = request.getQueryString();
						if (q != null && q.length() != 0)
						{
							buf.append('?');
							buf.append(q);
						}
						response.setContentLength(0);
						response.sendRedirect(response.encodeRedirectURL(buf.toString()));
					}
				}
				// else look for a welcome file
				else if (null != (welcome = getWelcomeFile(pathInContext)))
				{
					LOG.debug("welcome={}", welcome);
					if (_redirectWelcome)
					{
						// Redirect to the index
						response.setContentLength(0);
						String q = request.getQueryString();
						if (q != null && q.length() != 0)
							response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(), welcome) + "?" + q));
						else
							response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(), welcome)));
					}
					else
					{
						// Forward to the index
						RequestDispatcher dispatcher = request.getRequestDispatcher(welcome);
						if (dispatcher != null)
						{
							if (included.booleanValue())
								dispatcher.include(request, response);
							else
							{
								request.setAttribute("org.eclipse.jetty.server.welcome", welcome);
								dispatcher.forward(request, response);
							}
						}
					}
				}
				else
				{
					content = new HttpContent.ResourceAsHttpContent(resource, _mimeTypes.getMimeByExtension(resource.toString()));
					if (included.booleanValue() || passConditionalHeaders(request, response, resource, content))
						sendDirectory(request, response, resource, pathInContext);
				}
			}
		} catch (IllegalArgumentException e)
		{
			LOG.warn(Log.EXCEPTION, e);
			if (!response.isCommitted())
				response.sendError(500, e.getMessage());
		} finally
		{
			if (content != null)
				content.release();
			else if (resource != null)
				resource.release();
		}

	}

	/* ------------------------------------------------------------ */
	private boolean hasDefinedRange(Enumeration<String> reqRanges)
	{
		return (reqRanges != null && reqRanges.hasMoreElements());
	}

	/* ------------------------------------------------------------ */
	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException
	{
		doGet(request, response);
	}

	/* ------------------------------------------------------------ */
	/* (non-Javadoc)
	 * @see javax.servlet.http.HttpServlet#doTrace(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	@Override
	protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
	{
		resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
	}

	/* ------------------------------------------------------------ */
	@Override
	protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
		throws ServletException, IOException
	{
		resp.setHeader("Allow", "GET,HEAD,POST,OPTIONS");
	}

	/* ------------------------------------------------------------ */
	/**
	 * Finds a matching welcome file for the supplied {@link Resource}. This will be the first entry in the list of configured {@link #_welcomes welcome files} that existing within the directory referenced by the <code>Resource</code>. If the resource is not a directory, or no matching file is found, then it may look for a valid servlet mapping. If there is none, then <code>null</code> is returned. The list of welcome files is read from the {@link ContextHandler} for this servlet, or <code>"index.jsp" , "index.html"</code> if that is <code>null</code>.
	 * 
	 * @param resource
	 * @return The path of the matching welcome file in context or null.
	 * @throws IOException
	 * @throws MalformedURLException
	 */
	private String getWelcomeFile(String pathInContext) throws MalformedURLException, IOException
	{
		if (_welcomes == null)
			return null;

		String welcome_servlet = null;
		for (int i = 0; i < _welcomes.length; i++)
		{
			String welcome_in_context = URIUtil.addPaths(pathInContext, _welcomes[i]);
			Resource welcome = getResource(welcome_in_context);
			if (welcome != null && welcome.exists())
				return _welcomes[i];

			if ((_welcomeServlets || _welcomeExactServlets) && welcome_servlet == null)
			{
				Map.Entry entry = _servletHandler.getHolderEntry(welcome_in_context);
				if (entry != null && entry.getValue() != _defaultHolder &&
					(_welcomeServlets || (_welcomeExactServlets && entry.getKey().equals(welcome_in_context))))
					welcome_servlet = welcome_in_context;

			}
		}
		return welcome_servlet;
	}

	/* ------------------------------------------------------------ */
	/* Check modification date headers.
	 */
	protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, Resource resource, HttpContent content)
		throws IOException
	{
		try
		{
			if (!request.getMethod().equals(HttpMethods.HEAD))
			{
				String ifms = request.getHeader(HttpHeaders.IF_MODIFIED_SINCE);
				if (ifms != null)
				{
					if (content != null)
					{
						Buffer mdlm = content.getLastModified();
						if (mdlm != null)
						{
							if (ifms.equals(mdlm.toString()))
							{
								response.reset();
								response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
								response.flushBuffer();
								return false;
							}
						}
					}

					long ifmsl = request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
					if (ifmsl != -1)
					{
						if (resource.lastModified() / 1000 <= ifmsl / 1000)
						{
							response.reset();
							response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
							response.flushBuffer();
							return false;
						}
					}
				}

				// Parse the if[un]modified dates and compare to resource
				long date = request.getDateHeader(HttpHeaders.IF_UNMODIFIED_SINCE);

				if (date != -1)
				{
					if (resource.lastModified() / 1000 > date / 1000)
					{
						response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
						return false;
					}
				}

			}
		} catch (IllegalArgumentException iae)
		{
			if (!response.isCommitted())
				response.sendError(400, iae.getMessage());
			throw iae;
		}
		return true;
	}

	/* ------------------------------------------------------------------- */
	protected void sendDirectory(HttpServletRequest request,
		HttpServletResponse response,
		Resource resource,
		String pathInContext)
		throws IOException
	{
		if (!_dirAllowed)
		{
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return;
		}

		byte[] data = null;
		String base = URIUtil.addPaths(request.getRequestURI(), URIUtil.SLASH);

		// handle ResourceCollection
		if (_resourceBase instanceof ResourceCollection)
			resource = _resourceBase.addPath(pathInContext);
		else if (_contextHandler.getBaseResource() instanceof ResourceCollection)
			resource = _contextHandler.getBaseResource().addPath(pathInContext);

		String dir = resource.getListHTML(base, pathInContext.length() > 1);
		if (dir == null)
		{
			response.sendError(HttpServletResponse.SC_FORBIDDEN,
				"No directory");
			return;
		}

		data = dir.getBytes("UTF-8");
		response.setContentType("text/html; charset=UTF-8");
		response.setContentLength(data.length);
		response.getOutputStream().write(data);
	}

	/* ------------------------------------------------------------ */
	protected void sendData(HttpServletRequest request,
		HttpServletResponse response,
		boolean include,
		Resource resource,
		HttpContent content,
		Enumeration reqRanges)
		throws IOException
	{
		boolean direct;
		long content_length;
		if (content == null)
		{
			direct = false;
			content_length = resource.length();
		}
		else
		{
			Connector connector = HttpConnection.getCurrentConnection().getConnector();
			direct = connector instanceof NIOConnector && ((NIOConnector)connector).getUseDirectBuffers() && !(connector instanceof SslConnector);
			content_length = content.getContentLength();
		}

		// Get the output stream (or writer)
		OutputStream out = null;
		boolean written;
		try
		{
			out = response.getOutputStream();

			// has a filter already written to the response?
			written = out instanceof HttpOutput
				? ((HttpOutput)out).isWritten()
				: HttpConnection.getCurrentConnection().getGenerator().isWritten();
		} catch (IllegalStateException e)
		{
			out = new WriterOutputStream(response.getWriter());
			written = true; // there may be data in writer buffer, so assume written
		}

		if (reqRanges == null || !reqRanges.hasMoreElements() || content_length < 0)
		{
			//  if there were no ranges, send entire entity
			if (include)
			{
				resource.writeTo(out, 0, content_length);
			}
			else
			{
				// See if a direct methods can be used?
				if (content != null && !written && out instanceof HttpOutput)
				{
					if (response instanceof Response)
					{
						writeOptionHeaders(((Response)response).getHttpFields());
						((HttpConnection.Output)out).sendContent(content);
					}
					else
					{
						Buffer buffer = direct ? content.getDirectBuffer() : content.getIndirectBuffer();
						if (buffer != null)
						{
							writeHeaders(response, content, content_length);
							((HttpConnection.Output)out).sendContent(buffer);
						}
						else
						{
							writeHeaders(response, content, content_length);
							resource.writeTo(out, 0, content_length);
						}
					}
				}
				else
				{
					// Write headers normally
					writeHeaders(response, content, written ? -1 : content_length);

					// Write content normally
					Buffer buffer = (content == null) ? null : content.getIndirectBuffer();
					if (buffer != null)
						buffer.writeTo(out);
					else
						resource.writeTo(out, 0, content_length);
				}
			}
		}
		else
		{
			// Parse the satisfiable ranges
			List ranges = InclusiveByteRange.satisfiableRanges(reqRanges, content_length);

			//  if there are no satisfiable ranges, send 416 response
			if (ranges == null || ranges.size() == 0)
			{
				writeHeaders(response, content, content_length);
				response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
				response.setHeader(HttpHeaders.CONTENT_RANGE,
					InclusiveByteRange.to416HeaderRangeString(content_length));
				resource.writeTo(out, 0, content_length);
				return;
			}

			//  if there is only a single valid range (must be satisfiable
			//  since were here now), send that range with a 216 response
			if (ranges.size() == 1)
			{
				InclusiveByteRange singleSatisfiableRange =
					(InclusiveByteRange)ranges.get(0);
				long singleLength = singleSatisfiableRange.getSize(content_length);
				writeHeaders(response, content, singleLength);
				response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
				response.setHeader(HttpHeaders.CONTENT_RANGE,
					singleSatisfiableRange.toHeaderRangeString(content_length));
				resource.writeTo(out, singleSatisfiableRange.getFirst(content_length), singleLength);
				return;
			}

			//  multiple non-overlapping valid ranges cause a multipart
			//  216 response which does not require an overall
			//  content-length header
			//
			writeHeaders(response, content, -1);
			@SuppressWarnings("null")
			String mimetype = content.getContentType().toString();
			MultiPartOutputStream multi = new MultiPartOutputStream(out);
			response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

			// If the request has a "Request-Range" header then we need to
			// send an old style multipart/x-byteranges Content-Type. This
			// keeps Netscape and acrobat happy. This is what Apache does.
			String ctp;
			if (request.getHeader(HttpHeaders.REQUEST_RANGE) != null)
				ctp = "multipart/x-byteranges; boundary=";
			else
				ctp = "multipart/byteranges; boundary=";
			response.setContentType(ctp + multi.getBoundary());

			InputStream in = resource.getInputStream();
			long pos = 0;

			// calculate the content-length
			int length = 0;
			String[] header = new String[ranges.size()];
			for (int i = 0; i < ranges.size(); i++)
			{
				InclusiveByteRange ibr = (InclusiveByteRange)ranges.get(i);
				header[i] = ibr.toHeaderRangeString(content_length);
				length +=
					((i > 0) ? 2 : 0) +
						2 + multi.getBoundary().length() + 2 +
						HttpHeaders.CONTENT_TYPE.length() + 2 + mimetype.length() + 2 +
						HttpHeaders.CONTENT_RANGE.length() + 2 + header[i].length() + 2 +
						2 +
						(ibr.getLast(content_length) - ibr.getFirst(content_length)) + 1;
			}
			length += 2 + 2 + multi.getBoundary().length() + 2 + 2;
			response.setContentLength(length);

			for (int i = 0; i < ranges.size(); i++)
			{
				InclusiveByteRange ibr = (InclusiveByteRange)ranges.get(i);
				multi.startPart(mimetype, new String[] { HttpHeaders.CONTENT_RANGE + ": " + header[i] });

				long start = ibr.getFirst(content_length);
				long size = ibr.getSize(content_length);
				if (in != null)
				{
					// Handle non cached resource
					if (start < pos)
					{
						in.close();
						in = resource.getInputStream();
						pos = 0;
					}
					if (pos < start)
					{
						in.skip(start - pos);
						pos = start;
					}
					IO.copy(in, multi, size);
					pos += size;
				}
				else
					// Handle cached resource
					(resource).writeTo(multi, start, size);

			}
			if (in != null)
				in.close();
			multi.close();
		}
		return;
	}

	/* ------------------------------------------------------------ */
	protected void writeHeaders(HttpServletResponse response, HttpContent content, long count)
		throws IOException
	{
		if (content.getContentType() != null && response.getContentType() == null)
			response.setContentType(content.getContentType().toString());

		if (response instanceof Response)
		{
			Response r = (Response)response;
			HttpFields fields = r.getHttpFields();

			if (content.getLastModified() != null)
				fields.put(HttpHeaders.LAST_MODIFIED_BUFFER, content.getLastModified());
			else if (content.getResource() != null)
			{
				long lml = content.getResource().lastModified();
				if (lml != -1)
					fields.putDateField(HttpHeaders.LAST_MODIFIED_BUFFER, lml);
			}

			if (count != -1)
				r.setLongContentLength(count);

			writeOptionHeaders(fields);
		}
		else
		{
			long lml = content.getResource().lastModified();
			if (lml >= 0)
				response.setDateHeader(HttpHeaders.LAST_MODIFIED, lml);

			if (count != -1)
			{
				if (count < Integer.MAX_VALUE)
					response.setContentLength((int)count);
				else
					response.setHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(count));
			}

			writeOptionHeaders(response);
		}
	}

	/* ------------------------------------------------------------ */
	protected void writeOptionHeaders(HttpFields fields) throws IOException
	{
		if (_acceptRanges)
			fields.put(HttpHeaders.ACCEPT_RANGES_BUFFER, HttpHeaderValues.BYTES_BUFFER);

		if (_cacheControl != null)
			fields.put(HttpHeaders.CACHE_CONTROL_BUFFER, _cacheControl);
	}

	/* ------------------------------------------------------------ */
	protected void writeOptionHeaders(HttpServletResponse response) throws IOException
	{
		if (_acceptRanges)
			response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");

		if (_cacheControl != null)
			response.setHeader(HttpHeaders.CACHE_CONTROL, _cacheControl.toString());
	}

	/* ------------------------------------------------------------ */
	/*
	 * @see javax.servlet.Servlet#destroy()
	 */
	@Override
	public void destroy()
	{
		if (_cache != null)
			_cache.flushCache();
		super.destroy();
	}

}
